Reduce power consumption with display sleep and BLE optimizations#1
Open
thomassimmer wants to merge 12 commits intomainfrom
Open
Reduce power consumption with display sleep and BLE optimizations#1thomassimmer wants to merge 12 commits intomainfrom
thomassimmer wants to merge 12 commits intomainfrom
Conversation
Six root causes identified for 2-3h actual vs 13h design target: 1. BLE advertising ran indefinitely on first boot (no bonds) — add 120s pairing window timeout; user can reopen with Button B. 2. BLE connection interval used NimBLE default (~10-15 ms), waking the ESP32 radio 67-133×/s and negating light sleep while connected. Add update_conn_params(min=100ms, max=200ms, latency=4) in on_connect. 3. Display ST7789V2 controller stayed active when backlight turned off (3-5 mA wasted). Introduce DisplayPower trait with SLPIN/SLPOUT via mipidsi; call set_sleep_mode(true/false) at screen timeout/wake. 4. Fingerprint sensor RGB LED kept breathing in standby; add set_led(Off, Off) before set_work_mode(0) in FingerprintSensor::standby. 5. Screen timeout reduced from 30 s to 15 s (halves high-power active time). 6. Topbar and passive BLE-state draw calls now guarded by screen_on to avoid writing to the sleeping display controller. sdkconfig: enable BLE controller modem sleep (ORIG mode) to power down the radio between connection events. https://claude.ai/code/session_01U8fq2qAAfQi49AmvHZ64WT
The ESP-IDF UART driver holds an ESP_PM_APB_FREQ_MAX lock for its entire lifetime. UART1 (fingerprint) already had uart_set_wakeup_threshold + esp_sleep_enable_uart_wakeup configured, but UART0 (CLI / USB-C) did not. This prevented the PM driver from ever entering light sleep, leaving the CPU at 80 MHz WFI (~25 mA) instead of light sleep (~0.8 mA) — the single biggest contributor to the 2-3 h actual vs 13 h design target. Fix: apply the same wakeup-threshold pattern to UART_NUM_0. The first byte of any CLI command may be partially lost (start bit + ≤2 data bits), but the JSON framing causes the command to be rejected and the user can resend. Also remove the DisplayPower trait and its mipidsi impl: the internal mipidsi::interface::Interface trait is sealed and cannot be named from outside the crate, so the impl would not compile and was silently falling back to the old firmware. The screen-on guards on passive draw calls (topbar, BLE state changes) are kept — they avoid unnecessary SPI writes while the backlight is already off. https://claude.ai/code/session_01U8fq2qAAfQi49AmvHZ64WT
Every 60 s while the screen is off, app.rs now logs: - Battery % compared to the reading taken at screen-off, with an average current estimate (mA = 7200 × drop% / elapsed_s, based on the 200 mAh cell) so the actual drain rate is visible in serial logs. - esp_pm_dump_locks() output, which lists every active PM lock by name and type (APB_FREQ_MAX / NO_LIGHT_SLEEP / CPU_FREQ_MAX) so the root cause of any blocked light sleep is immediately identifiable. sdkconfig.defaults gains CONFIG_PM_PROFILING=y to make esp_pm_dump_locks() available at compile time. Look for "[DIAG]" lines on UART0 (USB-C serial, 115200 baud) after the screen has been off for ~1 minute. https://claude.ai/code/session_01U8fq2qAAfQi49AmvHZ64WT
esp_pm_dump_locks() requires a valid C FILE* — on ESP32/newlib stdout is not a simple global symbol so passing NULL causes fprintf to dereference it (LoadProhibited panic). Replace with a safe esp_get_minimum_free_heap_size() log line. The battery-drain mA calculation is unaffected and provides the key diagnostic: wait 15–20 min for a meaningful reading (battery integer resolution needs a ≥1% drop to register). https://claude.ai/code/session_01U8fq2qAAfQi49AmvHZ64WT
FreeRTOS tickless idle requires every task to be simultaneously blocked. With the NimBLE host task and UART driver tasks in the system this condition rarely if ever holds, so light sleep never fires despite being configured — confirmed by measuring ~42 mA drain (active-mode power) even with BLE disconnected and screen off. esp_light_sleep_start() called directly from the main task bypasses the tickless idle scheduler and puts the whole system to sleep immediately. All wakeup sources are already registered: - UART0 wakeup threshold (CLI commands) - UART1 wakeup threshold (fingerprint sensor) - BLE connection timer (CONFIG_BT_NIMBLE_RUN_BLE_ON_SUSPEND=y) - 100 ms timer (main loop poll rate when idle) If sleep is rejected by a NO_LIGHT_SLEEP lock the code falls back to FreeRtos::delay_ms so the loop keeps running; the DIAG log will then show non-zero mA which indicates a lock is still held. Expected result: ~1-3 mA idle (screen off, no BLE) vs current 42 mA. https://claude.ai/code/session_01U8fq2qAAfQi49AmvHZ64WT
esp_light_sleep_start() returns ESP_OK even when woken immediately by a spurious source (fingerprint sensor heartbeat on UART1 even in Timed Sleep mode triggers the UART1 wakeup threshold). The previous code only added a fallback delay on rejection, so on a quick-return success the loop spun at full CPU speed — measured at ~109 mA, worse than the 42 mA baseline. Fix: record esp_timer_get_time() before the sleep call and pad with a FreeRtos::delay_ms() for whatever time remains under IDLE_POLL_MS. This guarantees ≥100 ms per idle cycle regardless of why sleep ended, while still benefiting from real light sleep when the full 100 ms is slept (~1 mA vs ~35 mA active). https://claude.ai/code/session_01U8fq2qAAfQi49AmvHZ64WT
Root cause of ~42 mA idle drain (screen off, no BLE): on ESP32 the
automatic light sleep path via CONFIG_FREERTOS_USE_TICKLESS_IDLE only
works in single-core mode. In dual-core mode both cores must be
simultaneously idle before the PM system will enter light sleep; in
practice the NimBLE host task and UART driver tasks on Core 0 always
keep it slightly busy, so Core 1 being idle is never enough.
CONFIG_FREERTOS_UNICORE=y fixes this in two ways:
1. APP_CPU (Core 1) is powered off completely → ~10-15 mA saved
2. Tickless idle only needs PRO_CPU (Core 0) to be idle, which
happens naturally during the 100 ms FreeRtos::delay_ms() calls
Also revert the explicit esp_light_sleep_start() calls added in the
previous commits — they bypassed the PM subsystem coordination and
caused the BLE stack to reach an inconsistent state (~106 mA vs 42 mA
baseline), which is worse than simply relying on tickless idle.
Expected idle current after this change: ~1-5 mA (light sleep) vs the
current 42 mA, for a projected standby time of 13-40 h on 200 mAh.
https://claude.ai/code/session_01U8fq2qAAfQi49AmvHZ64WT
CONFIG_BT_CTRL_MODEM_SLEEP=y is incompatible with CONFIG_FREERTOS_UNICORE=y on ESP32: the BTDM controller crashes during init in single-core mode when modem sleep is enabled, causing a hard reboot loop measured at ~154 mA average (device never reaches the main loop). Remove both BT_CTRL_MODEM_SLEEP options. The power gain from unicore (Core 1 off, tickless idle now works) is much larger than the ~10 mA that modem sleep would have saved, so the net result is still better. Modem sleep can be revisited after confirming stable unicore operation. https://claude.ai/code/session_01U8fq2qAAfQi49AmvHZ64WT
Crash-loop diagnosis: removing BT_CTRL_MODEM_SLEEP alone (previous commit) did not stop the ~160 mA crash loop with FREERTOS_UNICORE=y. BT_NIMBLE_RUN_BLE_ON_SUSPEND requires the BTDM controller to support suspend/resume. Without BT_CTRL_MODEM_SLEEP that contract is broken, and the BTDM controller init asserts or panics on unicore where the controller task affinity assumptions may also differ from dual-core. Remove BT_NIMBLE_RUN_BLE_ON_SUSPEND. Trade-off: an active BLE connection will hold a NO_LIGHT_SLEEP lock, so idle current while connected rises from ~1 mA (full sleep) to ~35 mA (BLE active, unicore core-1 still powered off). Without a connection, light sleep is unrestricted and idle current is ~1 mA — the dominant use-case for a key that spends most time in a pocket or bag. https://claude.ai/code/session_01U8fq2qAAfQi49AmvHZ64WT
Without BT_NIMBLE_RUN_BLE_ON_SUSPEND the BT controller holds a NO_LIGHT_SLEEP PM lock permanently from init, making light sleep impossible even in unicore mode — confirmed at 109 mA idle drain. Re-add RUN_BLE_ON_SUSPEND to reproduce the unicore crash so the exact backtrace can be read from the serial monitor and the root cause fixed. Next step: flash, keep USB connected, read the Guru Meditation Error + backtrace from cargo run --release output. https://claude.ai/code/session_01U8fq2qAAfQi49AmvHZ64WT
Set the ESP-IDF "pm" log tag to VERBOSE so every lock acquire/release appears in the serial monitor. This lets us pinpoint which component is holding NO_LIGHT_SLEEP and preventing tickless idle light sleep, which is the root cause of the ~75 mA standby drain. Filter: cargo run --release 2>&1 | grep NO_LIGHT_SLEEP https://claude.ai/code/session_01U8fq2qAAfQi49AmvHZ64WT
Previous attempts with CONFIG_BT_CTRL_MODEM_SLEEP=y crashed in a reboot loop when combined with CONFIG_FREERTOS_UNICORE=y. The root cause is likely the BTDM controller failing to find the 32 kHz clock it needs for ORIG-mode sleep timing: the ESP32-PICO on M5StickC Plus 2 has no external 32 kHz crystal wired to the BT controller. CONFIG_BT_CTRL_SLEEP_CLOCK_USE_MAIN_XTAL=y redirects the sleep timer to the 40 MHz main XTAL (divided internally), which is always present. This should allow modem sleep without the crash and save ~20-25 mA of BLE radio idle power, bringing standby from ~43 mA toward the 15 mA target (0.8 mA ESP32 light sleep + 14 mA fingerprint sensor). Measured progression: - Baseline (no sleep) : ~75 mA (~2.7 h) - Unicore + RUN_BLE_ON_SUSPEND : ~43 mA (~4.6 h) - + modem sleep (this commit) : ~15 mA (~13 h, target) https://claude.ai/code/session_01U8fq2qAAfQi49AmvHZ64WT
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR reduces power consumption by implementing display controller sleep mode (SLPIN/SLPOUT), optimizing BLE connection parameters, and adding fingerprint sensor LED shutdown during standby.
Key Changes
Display Sleep Mode: Added
DisplayPowertrait to control display controller sleep state. The display now enters sleep mode when the screen times out, eliminating SPI oscillator current draw. Implemented viamipidsi::Display::sleep()andwake()methods.Screen Timeout Reduction: Reduced
SCREEN_TIMEOUT_TICKSfrom 1,500 (30s) to 750 (15s) to activate power-saving sleep mode sooner.Pairing Window Auto-Close: Added 120-second auto-close timer for the initial pairing window (when no bonds exist) to limit BLE advertising duration. Users can still reopen it with Button B.
Display Redraw Optimization: Only redraw display content when screen is on. Top bar updates and status displays now check
screen_onflag before calling display functions, preventing unnecessary operations while display is in sleep mode.BLE Connection Parameters: Added
server.update_conn_params()call on connection to request longer intervals (100-200 ms) and latency (4 events), reducing radio wakeup frequency. The host OS may negotiate different values but this signals intent.BLE Modem Sleep: Enabled
CONFIG_BT_CTRL_MODEM_SLEEPandCONFIG_BT_CTRL_MODEM_SLEEP_MODE_1in sdkconfig to power down the BLE radio between connection events, saving 10-20 mA while connected.Fingerprint Sensor LED: Turn off the LED ring before entering standby mode to avoid unnecessary current draw during idle periods.
Implementation Details
wake_screen_if_off()function now takes adispparameter and callsdisp.set_sleep_mode(false)before enabling the backlight, ensuring the display controller is awake before SPI communication.set_sleep_mode(true)when screen times out,set_sleep_mode(false)when user activity resumes.DisplayPowertrait is generic over mipidsi's interface and model types, allowing it to work with any display configuration.https://claude.ai/code/session_01U8fq2qAAfQi49AmvHZ64WT